/**
 * 
 */
package edu.umd.clip.lm.model;

import java.io.*;
import java.nio.*;
import java.nio.channels.*;

import org.w3c.dom.*;

import edu.umd.clip.lm.util.*;
/**
 * @author Denis Filimonov <den@cs.umd.edu>
 *
 */
public class FileClusterStorage extends AbstractClusterStorage {
	private static final String XML_ATTR_MAX_CLUSTER_ID_NAME = "max-cluster-id";
	private static final String XML_LOCATIONS_NAME = "locations";
	private static final String XML_FILENAME = "filename";

	private String filename;
	private FileChannel channel; 
	private ClusterLocation[] locations;
	private int maxClusterId = 0;
	
	public static class ClusterLocation extends XMLizable {
		public final static String XML_ELEMENT_NAME = "loc";
		private final static String XML_ATTR_OFFSET_NAME = "offset";
		private final static String XML_ATTR_LENGTH_NAME = "len";
		private final static String XML_ATTR_CLUSTER_ID_NAME = "id";
		
		private long offset;
		private int length;
		
		public ClusterLocation(long offset, int length) {
			this.offset = offset;
			this.length = length;
		}

		public ClusterLocation(Element xmlnode) {
			offset = Long.parseLong(xmlnode.getAttribute(XML_ATTR_OFFSET_NAME));
			length = Integer.parseInt(xmlnode.getAttribute(XML_ATTR_LENGTH_NAME));
		}
		
		@Override
		public Element createXML(Document doc) {
			Element element = doc.createElement(XML_ELEMENT_NAME);
			element.setAttribute(XML_ATTR_OFFSET_NAME, Long.toString(offset));
			element.setAttribute(XML_ATTR_LENGTH_NAME, Integer.toString(length));
			return element;
		}
		
		@Override
		public String getElementName() {
			return XML_ELEMENT_NAME;
		}
		
		@Override
		public boolean equals(Object obj) {
			if (obj instanceof ClusterLocation) {
				ClusterLocation that = (ClusterLocation) obj;
				return that.offset == offset && that.length == length;
			}
			return false;
		}

		@Override
		public int hashCode() {
			return (int) offset;
		}

		@Override
		public String toString() {
			return String.format("[%d-%d]", offset, length);
		}

		public long getOffset() {
			return offset;
		}
		
		public int getLength() {
			return length;
		}
	}
	
	public FileClusterStorage(String filename) {
		super();
		this.maxClusterId = 0;
		this.filename = filename;
	}

	public FileClusterStorage(Element xmlnode) {
		maxClusterId = Integer.parseInt(xmlnode.getAttribute(XML_ATTR_MAX_CLUSTER_ID_NAME));
		
		Element node = XMLUtils.getFirstElementByTagName(xmlnode, XML_FILENAME);
		filename = node.getTextContent();
		
		node = XMLUtils.getFirstElementByTagName(xmlnode, XML_LOCATIONS_NAME);
		if (node != null) {
			locations = new ClusterLocation[maxClusterId+1];
			NodeList nodes = XMLUtils.getChildrenElementsByTagName(node, ClusterLocation.XML_ELEMENT_NAME);
			for(int i=0; i<nodes.getLength(); ++i) {
				Element location = (Element)nodes.item(i);
				int clusterid = Integer.parseInt(location.getAttribute(ClusterLocation.XML_ATTR_CLUSTER_ID_NAME));
				assert(clusterid <= maxClusterId);
				locations[clusterid] = new ClusterLocation(location);
			}
		}
	}
	
	public void openForReading() {
		if (channel == null) {
			try {
				synchronized(this) {
					if (channel == null) {
						channel = new RandomAccessFile(filename, "r").getChannel();
					}
				}
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				System.exit(1);
			}
		}
	}
	
	public void openForWriting(int maxClusters) {
		try {
			RandomAccessFile file = new RandomAccessFile(filename, "rw");
			file.setLength(0);
			channel = file.getChannel();
			locations = new ClusterLocation[maxClusters];
		} catch(IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
	}
	
	@Override
	public void close() {
		if (channel != null) {
			try {
				channel.close();
				channel = null;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	@Override
	public boolean isOpen() {
		return channel != null;
	}
	
	public void addCluster(int clusterid, ByteBuffer data) {
		try {
			synchronized(this) {
				if (locations[clusterid] != null) {
					System.out.printf("cluster #%d already stored", clusterid);
					return;
				}
				
				long position = channel.position();
				locations[clusterid] = new ClusterLocation(position, data.limit());
				channel.write(data);
				if (maxClusterId < clusterid) maxClusterId = clusterid;
			}
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
		
	}
	/* (non-Javadoc)
	 * @see edu.umd.clip.lm.model.AbstractClusterStorage#loadCluster(int)
	 */
	@Override
	public ByteBuffer loadCluster(int clusterid) {
		if (clusterid >= locations.length || locations[clusterid] == null) {
			throw new Error(String.format("bad cluster/location: clusterid=%d, nr-locations=%d, %s\n", 
					clusterid, locations.length, this.toString()));
		}
		ClusterLocation location = locations[clusterid];
		ByteBuffer buffer = ByteBuffer.allocate(location.getLength());
		try {
			channel.read(buffer, location.getOffset());
		} catch (IOException e) {
			e.printStackTrace();
			System.exit(1);
		}
		return buffer;
	}

	@Override
	public Element createXML(Document doc) {
		Element element = super.createXML(doc);
		element.setAttribute(XML_ATTR_MAX_CLUSTER_ID_NAME, Integer.toString(maxClusterId));
		
		Element node = doc.createElement(XML_FILENAME);
		node.setTextContent(filename);
		element.appendChild(node);
		
		if (locations != null) {
			node = doc.createElement(XML_LOCATIONS_NAME);
			for(int clusterid = 0; clusterid < locations.length; ++clusterid) {
				if (locations[clusterid] == null) continue;
				
				Element loc = locations[clusterid].createXML(doc);
				loc.setAttribute(ClusterLocation.XML_ATTR_CLUSTER_ID_NAME, Integer.toString(clusterid));
				node.appendChild(loc);
			}
			element.appendChild(node);
		}
		return element;
	}

	@Override
	public int getType() {
		return FILE_STORAGE_TYPE;
	}

	@Override
	public int getMaxClusterId() {
		return maxClusterId;
	}
}
